home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 1865 / 1865.xpi / chrome / adblockplus.jar / content / synchronizer.js < prev    next >
Text File  |  2010-01-07  |  14KB  |  466 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is Adblock Plus.
  15.  *
  16.  * The Initial Developer of the Original Code is
  17.  * Wladimir Palant.
  18.  * Portions created by the Initial Developer are Copyright (C) 2006-2009
  19.  * the Initial Developer. All Rights Reserved.
  20.  *
  21.  * Contributor(s):
  22.  *
  23.  * ***** END LICENSE BLOCK ***** */
  24.  
  25. /**
  26.  * @fileOverview Manages synchronization of filter subscriptions.
  27.  * This file is included from AdblockPlus.js.
  28.  */
  29.  
  30. var XMLHttpRequest = Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1", "nsIJSXMLHttpRequest");
  31.  
  32. /**
  33.  * This object is responsible for downloading filter subscriptions whenever
  34.  * necessary.
  35.  * @class
  36.  */
  37. var synchronizer =
  38. {
  39.     /**
  40.      * Map of subscriptions currently being downloaded, all currently downloaded
  41.      * URLs are keys of that map.
  42.      */
  43.     executing: {__proto__: null},
  44.  
  45.     /**
  46.      * Initializes synchronizer so that it checks hourly whether any subscriptions
  47.      * need to be downloaded.
  48.      */
  49.     init: function()
  50.     {
  51.         let me = this;
  52.         let callback = function()
  53.         {
  54.             me.timer.delay = 3600000;
  55.             me.checkSubscriptions();
  56.         };
  57.  
  58.         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
  59.         this.timer.initWithCallback(callback, 300000, Ci.nsITimer.TYPE_REPEATING_SLACK);
  60.     },
  61.  
  62.     /**
  63.      * Checks whether any subscriptions need to be downloaded and starts the download
  64.      * if necessary.
  65.      */
  66.     checkSubscriptions: function()
  67.     {
  68.         let time = Date.now()/1000;
  69.         for each (let subscription in filterStorage.subscriptions)
  70.         {
  71.             if (!(subscription instanceof DownloadableSubscription) || !subscription.autoDownload)
  72.                 continue;
  73.     
  74.             if (subscription.expires > time)
  75.                 continue;
  76.  
  77.             // Get the number of hours since last download
  78.             let interval = (time - subscription.lastDownload) / 3600;
  79.             if (interval >= prefs.synchronizationinterval)
  80.                 synchronizer.execute(subscription);
  81.         }
  82.     },
  83.  
  84.     /**
  85.      * Checks whether a subscription is currently being downloaded.
  86.      * @param {String} url  URL of the subscription
  87.      * @return {Boolean}
  88.      */
  89.     isExecuting: function(url)
  90.     {
  91.         return url in this.executing;
  92.     },
  93.  
  94.     /**
  95.      * Extracts a list of filters from text returned by a server.
  96.      * @param {DownloadableSubscription} subscription  subscription the info should be placed into
  97.      * @param {String} text server response
  98.      * @param {Function} errorCallback function to be called on error
  99.      * @return {Array of Filter}
  100.      */
  101.     readFilters: function(subscription, text, errorCallback)
  102.     {
  103.         let lines = text.split(/[\r\n]+/);
  104.         if (!/\[Adblock(?:\s*Plus\s*([\d\.]+)?)?\]/i.test(lines[0]))
  105.         {
  106.             errorCallback("synchronize_invalid_data");
  107.             return null;
  108.         }
  109.         let minVersion = RegExp.$1;
  110.  
  111.         for (let i = 0; i < lines.length; i++)
  112.         {
  113.             if (/!\s*checksum[\s\-:]+([\w\+\/]+)/i.test(lines[i]))
  114.             {
  115.                 lines.splice(i, 1);
  116.                 let checksumExpected = RegExp.$1;
  117.                 let checksum = generateChecksum(lines);
  118.  
  119.                 if (checksum && checksum != checksumExpected)
  120.                 {
  121.                     errorCallback("synchronize_checksum_mismatch");
  122.                     return null;
  123.                 }
  124.  
  125.                 break;
  126.             }
  127.         }
  128.  
  129.         delete subscription.requiredVersion;
  130.         delete subscription.upgradeRequired;
  131.         if (minVersion)
  132.         {
  133.             subscription.requiredVersion = minVersion;
  134.             if (abp.versionComparator.compare(minVersion, abp.getInstalledVersion()) > 0)
  135.                 subscription.upgradeRequired = true;
  136.         }
  137.  
  138.         lines.shift();
  139.         let result = [];
  140.         for each (let line in lines)
  141.         {
  142.             let filter = Filter.fromText(normalizeFilter(line));
  143.             if (filter)
  144.                 result.push(filter);
  145.         }
  146.  
  147.         return result;
  148.     },
  149.  
  150.     /**
  151.      * Handles an error during a subscription download.
  152.      * @param {DownloadableSubscription} subscription  subscription that failed to download
  153.      * @param {Integer} channelStatus result code of the download channel
  154.      * @param {String} responseStatus result code as received from server
  155.      * @param {String} downloadURL the URL used for download
  156.      * @param {String} error error ID in global.properties
  157.      * @param {Boolean} isBaseLocation false if the subscription was downloaded from a location specified in X-Alternative-Locations header
  158.      */
  159.     setError: function(subscription, error, channelStatus, responseStatus, downloadURL, isBaseLocation)
  160.     {
  161.         // If download from an alternative location failed, reset the list of
  162.         // alternative locations - have to get an updated list from base location.
  163.         if (!isBaseLocation)
  164.             subscription.alternativeLocations = null;
  165.  
  166.         subscription.lastDownload = parseInt(Date.now() / 1000);
  167.         subscription.downloadStatus = error;
  168.         if (error == "synchronize_checksum_mismatch")
  169.         {
  170.             // No fallback for successful download with checksum mismatch, reset error counter
  171.             subscription.errors = 0;
  172.         }
  173.         else
  174.             subscription.errors++;
  175.  
  176.         if (subscription.errors >= prefs.subscriptions_fallbackerrors && /^https?:\/\//i.test(subscription.url))
  177.         {
  178.             subscription.errors = 0;
  179.  
  180.             let fallbackURL = prefs.subscriptions_fallbackurl;
  181.             fallbackURL = fallbackURL.replace(/%SUBSCRIPTION%/g, encodeURIComponent(subscription.url));
  182.             fallbackURL = fallbackURL.replace(/%URL%/g, encodeURIComponent(downloadURL));
  183.             fallbackURL = fallbackURL.replace(/%ERROR%/g, encodeURIComponent(error));
  184.             fallbackURL = fallbackURL.replace(/%CHANNELSTATUS%/g, encodeURIComponent(channelStatus));
  185.             fallbackURL = fallbackURL.replace(/%RESPONSESTATUS%/g, encodeURIComponent(responseStatus));
  186.  
  187.             let request = new XMLHttpRequest();
  188.             request.open("GET", fallbackURL);
  189.             request.overrideMimeType("text/plain");
  190.             request.channel.loadGroup = null;
  191.             request.channel.loadFlags = request.channel.loadFlags |
  192.                                                                     request.channel.INHIBIT_CACHING |
  193.                                                                     request.channel.VALIDATE_ALWAYS;
  194.             request.onload = function(ev)
  195.             {
  196.                 if (/^301\s+(\S+)/.test(request.responseText))  // Moved permanently    
  197.                     subscription.nextURL = RegExp.$1;
  198.                 else if (/^410\b/.test(request.responseText))   // Gone
  199.                 {
  200.                     subscription.autoDownload = false;
  201.                     filterStorage.triggerSubscriptionObservers("updateinfo", [subscription]);
  202.                 }
  203.                 filterStorage.saveToDisk();
  204.             }
  205.             request.send(null);
  206.         }
  207.  
  208.         filterStorage.triggerSubscriptionObservers("updateinfo", [subscription]);
  209.         filterStorage.saveToDisk();
  210.     },
  211.  
  212.     /**
  213.      * Starts the download of a subscription.
  214.      * @param {DownloadableSubscription} subscription  Subscription to be downloaded
  215.      * @param {Boolean}  forceDownload  if true, the subscription will even be redownloaded if it didn't change on the server
  216.      */
  217.     execute: function(subscription, forceDownload)
  218.     {
  219.         let url = subscription.url;
  220.         if (url in this.executing)
  221.             return;
  222.  
  223.         let newURL = subscription.nextURL;
  224.         let hadTemporaryRedirect = false;
  225.         subscription.nextURL = null;
  226.  
  227.         let curVersion = abp.getInstalledVersion();
  228.         let loadFrom = newURL;
  229.         let isBaseLocation = true;
  230.         if (!loadFrom)
  231.         {
  232.             loadFrom = url;
  233.             if (subscription.alternativeLocations)
  234.             {
  235.                 // We have alternative download locations, choose one. "Regular"
  236.                 // subscription URL always goes in with weight 1.
  237.                 let options = [[1, url]];
  238.                 let totalWeight = 1;
  239.                 for each (let alternative in subscription.alternativeLocations.split(','))
  240.                 {
  241.                     if (!/^https?:\/\//.test(alternative))
  242.                         continue;
  243.  
  244.                     let weight = 1;
  245.                     let weightingRegExp = /;q=([\d\.]+)$/;
  246.                     if (weightingRegExp.test(alternative))
  247.                     {
  248.                         weight = parseFloat(RegExp.$1);
  249.                         if (isNaN(weight) || !isFinite(weight) || weight < 0)
  250.                             weight = 1;
  251.                         if (weight > 10)
  252.                             weight = 10;
  253.  
  254.                         alternative = alternative.replace(weightingRegExp, "");
  255.                     }
  256.                     options.push([weight, alternative]);
  257.                     totalWeight += weight;
  258.                 }
  259.  
  260.                 let choice = Math.random() * totalWeight;
  261.                 for each (let [weight, alternative] in options)
  262.                 {
  263.                     choice -= weight;
  264.                     if (choice < 0)
  265.                     {
  266.                         loadFrom = alternative;
  267.                         break;
  268.                     }
  269.                 }
  270.  
  271.                 isBaseLocation = (loadFrom == url);
  272.             }
  273.         }
  274.         loadFrom = loadFrom.replace(/%VERSION%/, "ABP" + curVersion);
  275.  
  276.         let request = null;
  277.         let me = this;
  278.         function errorCallback(error)
  279.         {
  280.             let channelStatus = -1;
  281.             try {
  282.                 channelStatus = request.channel.status;
  283.             } catch (e) {}
  284.             let responseStatus = "";
  285.             try {
  286.                 responseStatus = request.channel.QueryInterface(Ci.nsIHttpChannel).responseStatus;
  287.             } catch (e) {}
  288.             me.setError(subscription, error, channelStatus, responseStatus, loadFrom, isBaseLocation);
  289.         }
  290.  
  291.         try {
  292.             request = new XMLHttpRequest();
  293.             request.open("GET", loadFrom);
  294.         }
  295.         catch (e) {
  296.             errorCallback("synchronize_invalid_url");
  297.             return;
  298.         }
  299.  
  300.         try {
  301.             request.overrideMimeType("text/plain");
  302.             request.channel.loadGroup = null;
  303.             request.channel.loadFlags = request.channel.loadFlags |
  304.                                                                     request.channel.INHIBIT_CACHING |
  305.                                                                     request.channel.VALIDATE_ALWAYS;
  306.  
  307.             // Override redirect limit from preferences, user might have set it to 1
  308.             if (request.channel instanceof Ci.nsIHttpChannel)
  309.                 request.channel.redirectionLimit = 5;
  310.  
  311.             var oldNotifications = request.channel.notificationCallbacks;
  312.             var oldEventSink = null;
  313.             request.channel.notificationCallbacks =
  314.             {
  315.                 QueryInterface: XPCOMUtils.generateQI([Ci.nsIChannelEventSink]),
  316.  
  317.                 getInterface: function(iid)
  318.                 {
  319.                     if (iid.equals(Ci.nsIChannelEventSink))
  320.                     {
  321.                         try {
  322.                             oldEventSink = oldNotifications.QueryInterface(iid);
  323.                         } catch(e) {}
  324.                         return this;
  325.                     }
  326.         
  327.                     return (oldNotifications ? oldNotifications.QueryInterface(iid) : null);
  328.                 },
  329.  
  330.                 onChannelRedirect: function(oldChannel, newChannel, flags)
  331.                 {
  332.                     if (isBaseLocation && !hadTemporaryRedirect && oldChannel instanceof Ci.nsIHttpChannel)
  333.                     {
  334.                         try {
  335.                             subscription.alternativeLocations = oldChannel.getResponseHeader("X-Alternative-Locations");
  336.                         }
  337.                         catch (e) {
  338.                             subscription.alternativeLocations = null;
  339.                         }
  340.                     }
  341.  
  342.                     if (flags & Ci.nsIChannelEventSink.REDIRECT_TEMPORARY)
  343.                         hadTemporaryRedirect = true;
  344.                     else if (!hadTemporaryRedirect)
  345.                         newURL = newChannel.URI.spec;
  346.  
  347.                     if (oldEventSink)
  348.                         oldEventSink.onChannelRedirect(oldChannel, newChannel, flags);
  349.                 }
  350.             }
  351.         } catch (e) {}
  352.  
  353.         if (subscription.lastModified && !forceDownload)
  354.             request.setRequestHeader("If-Modified-Since", subscription.lastModified);
  355.             this.request = request;
  356.  
  357.         request.onerror = function(ev)
  358.         {
  359.             delete me.executing[url];
  360.             try {
  361.                 request.channel.notificationCallbacks = null;
  362.             } catch (e) {}
  363.  
  364.             errorCallback("synchronize_connection_error");
  365.         };
  366.  
  367.         request.onload = function(ev)
  368.         {
  369.             delete me.executing[url];
  370.             try {
  371.                 request.channel.notificationCallbacks = null;
  372.             } catch (e) {}
  373.  
  374.             // Status will be 0 for non-HTTP requests
  375.             if (request.status && request.status != 200 && request.status != 304)
  376.             {
  377.                 errorCallback("synchronize_connection_error");
  378.                 return;
  379.             }
  380.  
  381.             let newFilters = null;
  382.             if (request.status != 304)
  383.             {
  384.                 newFilters = me.readFilters(subscription, request.responseText, errorCallback);
  385.                 if (!newFilters)
  386.                     return;
  387.  
  388.                 subscription.lastModified = request.getResponseHeader("Last-Modified");
  389.             }
  390.  
  391.             if (isBaseLocation && !hadTemporaryRedirect)
  392.                 subscription.alternativeLocations = request.getResponseHeader("X-Alternative-Locations");
  393.             subscription.lastDownload = parseInt(Date.now() / 1000);
  394.             subscription.downloadStatus = "synchronize_ok";
  395.             subscription.errors = 0;
  396.  
  397.             let expires = parseInt(new Date(request.getResponseHeader("Expires")).getTime() / 1000) || 0;
  398.             for each (let filter in newFilters)
  399.             {
  400.                 if (filter instanceof CommentFilter && /\bExpires\s*(?::|after)\s*(\d+)\s*(h)?/i.test(filter.text))
  401.                 {
  402.                     var hours = parseInt(RegExp.$1);
  403.                     if (!RegExp.$2)
  404.                         hours *= 24;
  405.                     if (hours > 0)
  406.                     {
  407.                         let time = subscription.lastDownload + hours * 3600;
  408.                         if (time > expires)
  409.                             expires = time;
  410.                     }
  411.                 }
  412.                 if (isBaseLocation && filter instanceof CommentFilter && /\bRedirect(?:\s*:\s*|\s+to\s+|\s+)(\S+)/i.test(filter.text))
  413.                     subscription.nextURL = RegExp.$1;
  414.             }
  415.             subscription.expires = (expires > subscription.lastDownload ? expires : 0);
  416.  
  417.             // Expiration date shouldn't be more than two weeks in the future
  418.             if (subscription.expires - subscription.lastDownload > 14*24*3600)
  419.                 subscription.expires = subscription.lastDownload + 14*24*3600;
  420.  
  421.             if (isBaseLocation && newURL && newURL != url)
  422.             {
  423.                 let listed = (subscription.url in filterStorage.knownSubscriptions);
  424.                 if (listed)
  425.                     filterStorage.removeSubscription(subscription);
  426.  
  427.                 url = newURL;
  428.  
  429.                 let newSubscription = Subscription.fromURL(url);
  430.                 for (let key in newSubscription)
  431.                     delete newSubscription[key];
  432.                 for (let key in subscription)
  433.                     newSubscription[key] = subscription[key];
  434.  
  435.                 delete Subscription.knownSubscriptions[subscription.url];
  436.                 newSubscription.oldSubscription = subscription;
  437.                 subscription = newSubscription;
  438.                 subscription.url = url;
  439.  
  440.                 if (!(subscription.url in filterStorage.knownSubscriptions) && listed)
  441.                     filterStorage.addSubscription(subscription);
  442.             }
  443.  
  444.             if (newFilters)
  445.                 filterStorage.updateSubscriptionFilters(subscription, newFilters);
  446.             else
  447.                 filterStorage.triggerSubscriptionObservers("updateinfo", [subscription]);
  448.             delete subscription.oldSubscription;
  449.  
  450.             filterStorage.saveToDisk();
  451.         };
  452.  
  453.         this.executing[url] = true;
  454.         filterStorage.triggerSubscriptionObservers("updateinfo", [subscription]);
  455.  
  456.         try {
  457.             request.send(null);
  458.         }
  459.         catch (e) {
  460.             errorCallback("synchronize_connection_error");
  461.             return;
  462.         }
  463.     }
  464. };
  465. abp.synchronizer = synchronizer;
  466.